Исследование рынка заведений общественного питания Москвы¶

Содержание

  • 1  Загрузка данных и подготовка к анализу
  • 2  Предобработка данных
    • 2.1  Обработка дубликатов
    • 2.2  Обработка пропусков
    • 2.3  Добавление новых столбцов
  • 3  Исследовательский анализ данных
    • 3.1  Распределение заведений по категориям
    • 3.2  Распределение посадочных мест по категориям
    • 3.3  Распределение сетевых и несетевых заведений
    • 3.4  Топ-15 популярных сетей в Москве
    • 3.5  Распределение заведений по районам Москвы
    • 3.6  Распределение средних рейтингов по категориям заведений
    • 3.7  Распределение средних рейтингов по районам Москвы
    • 3.8  Распределение заведений по улицам Москвы
    • 3.9  Стоимость среднего чека в зависимости от района
    • 3.10  Зависимость режима работы от категорий и районов
    • 3.11  Особенности заведений с плохими рейтингами
  • 4  Целесообразность открытия кофейни
    • 4.1  Особенности расположения кофеен
    • 4.2  Режим работы кофеен
    • 4.3  Распределение рейтингов кофеен по районам
    • 4.4  Распределение средней стоимости чашки капучино по районам
    • 4.5  Рекомендации по открытию кофейни
  • 5  Выводы

Краткое описание проекта: инвесторы из фонда «Shut Up and Take My Money» решили попробовать себя в новой области и открыть заведение общественного питания в Москве. Для этого необходимо подготовить исследование рынка Москвы, найти интересные особенности и презентовать полученные результаты, которые в будущем помогут в выборе подходящего инвесторам места.

Описание данных:

  • name — название заведения;
  • address — адрес заведения;
  • category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
  • hours — информация о днях и часах работы;
  • lat — широта географической точки, в которой находится заведение;
  • lng — долгота географической точки, в которой находится заведение;
  • rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
  • price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
  • avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:
  • «Средний счёт: 1000–1500 ₽»;
  • «Цена чашки капучино: 130–220 ₽»;
  • «Цена бокала пива: 400–600 ₽».
    и так далее;
  • middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»:
  • Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
  • Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
  • Если значения нет или оно не начинается с подстроки «Средний счёт», то в столбец ничего не войдёт.
  • middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»:
  • Если в строке указан ценовой диапазон из двух значений, в столбец войдёт медиана этих двух значений.
  • Если в строке указано одно число — цена без диапазона, то в столбец войдёт это число.
  • Если значения нет или оно не начинается с подстроки «Цена одной чашки капучино», то в столбец ничего не войдёт.
  • chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):
  • 0 — заведение не является сетевым
  • 1 — заведение является сетевым
  • district — административный район, в котором находится заведение, например Центральный административный округ;
  • seats — количество посадочных мест.

План исследования:

  1. Выгрузка данных и первичное знакомство с данными.
  2. Изменение типов данных, обработка дубликатов и пропусков, добавление новых столбцов.
  3. Анализ распределения данных по категориям заведений.
  4. Выделение топ-15 популярных сетей общепита Москвы.
  5. Анализ распределения данных по районам заведений.
  6. Исследование зависимости режима работы от категории заведения.
  7. Выделение особенностей заведений с низким рейтингом.
  8. Исследование целесообразности открытия кофейни.
  9. Выводы.

Цель проекта: исследование рынка общепита Москвы и целесообразности открытия кофейни.

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import datetime as dt
import plotly.express as px
import folium
import re
import json
from scipy import stats as st
from statsmodels.stats import proportion as pr
from plotly import graph_objects as go
from scipy.stats import binom, norm
from math import sqrt
from IPython.display import display
from IPython.display import Image
from folium import Map, Marker, Choropleth
from folium.plugins import MarkerCluster

Загрузка данных и подготовка к анализу¶

In [2]:
catering = pd.read_csv('moscow_places.csv')
In [3]:
catering.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8406 entries, 0 to 8405
Data columns (total 14 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   name               8406 non-null   object 
 1   category           8406 non-null   object 
 2   address            8406 non-null   object 
 3   district           8406 non-null   object 
 4   hours              7870 non-null   object 
 5   lat                8406 non-null   float64
 6   lng                8406 non-null   float64
 7   rating             8406 non-null   float64
 8   price              3315 non-null   object 
 9   avg_bill           3816 non-null   object 
 10  middle_avg_bill    3149 non-null   float64
 11  middle_coffee_cup  535 non-null    float64
 12  chain              8406 non-null   int64  
 13  seats              4795 non-null   float64
dtypes: float64(6), int64(1), object(7)
memory usage: 919.5+ KB
In [4]:
print(f'Количество заведений: {catering["name"].nunique()}')
Количество заведений: 5614

Общее количество заведений в датасете составляет 5614. Из предварительного анализа видно, что в некоторых столбцах есть пропуски. Тип данных всех столбцов соответствует содержащейся в ней информации.

Предобработка данных¶

Обработка дубликатов¶

In [5]:
print(f'Количество дубликатов: {catering.duplicated().sum()}')
Количество дубликатов: 0
In [6]:
def name_standard(text): # функция для стандартизации названий
    text = text.replace('ё', 'е')
    text = text.replace('«', '')
    text = text.replace('»', '')
    text = text.lower()
    dict =['кафе ', 'ресторан ', 'бар ', 'столовая ', 'кофейня ', 'пиццерия ', 'закусочная ']
    for i in dict:
        text = text.replace(i, '')  
    return text
In [7]:
catering['name_standard'] = catering['name'].apply(name_standard) #создаем новый столбец со стандартизированным названием
In [8]:
print(f'Количество неявных дубликатов: {catering[["name_standard","address", "seats"]].duplicated().sum()}')
Количество неявных дубликатов: 6
In [9]:
catering = catering.drop_duplicates(subset=['name_standard', 'address', 'seats']) # удаляем неявные дубликаты

Явные дубликаты в данных отсутствуют, неявные в количестве 6 штук были удалены.

Обработка пропусков¶

In [10]:
null_ratio = pd.DataFrame(round(catering.isna().mean()*100,1))
null_ratio.columns = ['null_ratio']
null_ratio
Out[10]:
null_ratio
name 0.0
category 0.0
address 0.0
district 0.0
hours 6.4
lat 0.0
lng 0.0
rating 0.0
price 60.5
avg_bill 54.6
middle_avg_bill 62.5
middle_coffee_cup 93.6
chain 0.0
seats 43.0
name_standard 0.0
In [11]:
catering['hours'] = catering['hours'].replace('Нет информации', 'NaN')

Пропуски в столбце hours заполнить невозможно. Они составляют 6,4% от общего количества записей, поэтому лучше их не удалять, так как возможна потеря важных данных.

Пропуски ценовой категории заведения в столбце price можно заполнить в соответствии с диапазонами значений среднего чека и ценой чашечки кофе в каждой из категории и районов:

In [12]:
catering['min'] = catering.groupby(['district', 'category'])['middle_avg_bill'].transform('min')
catering['max'] = catering.groupby(['district', 'category'])['middle_avg_bill'].transform('max')
catering['interval'] = (catering['max'] - catering['min']) / 4
In [13]:
def fill_na_price(row):
    try:
        if row['middle_avg_bill'] <= row['min'] + row['interval']:
            return 'низкие'
        elif row['middle_avg_bill'] <= row['min'] + 2 * row['interval']:
            return 'средние'
        elif row['middle_avg_bill'] <= row['min'] + 3 * row['interval']:
            return 'выше среднего'
        elif row['middle_avg_bill'] > row['min'] + 3 * row['interval']:
            return 'высокие'
    except:
        pass 
In [14]:
catering['price'] = catering['price'].fillna(catering[['min', 'interval', 
                                                       'middle_avg_bill']].apply(fill_na_price, axis=1))
In [15]:
catering.drop(columns=['min', 'max', 'interval'], inplace=True)
In [16]:
catering['min'] = catering.groupby(['district', 'category'])['middle_coffee_cup'].transform('min')
catering['max'] = catering.groupby(['district', 'category'])['middle_coffee_cup'].transform('max')
catering['interval'] = (catering['max'] - catering['min']) / 4
In [17]:
def fill_na_price(row):
    try:
        if row['middle_coffee_cup'] <= row['min'] + row['interval']:
            return 'низкие'
        elif row['middle_coffee_cup'] <= row['min'] + 2 * row['interval']:
            return 'средние'
        elif row['middle_coffee_cup'] <= row['min'] + 3 * row['interval']:
            return 'выше среднего'
        elif row['middle_coffee_cup'] > row['min'] + 3 * row['interval']:
            return 'высокие'
    except:
        pass 
In [18]:
catering['price'] = catering['price'].fillna(catering[['min', 'interval', 
                                                       'middle_coffee_cup']].apply(fill_na_price, axis=1))
In [19]:
catering.drop(columns=['min', 'max', 'interval'], inplace=True)

Посчитаем количество пропусков в price, которые можно заполнить на основании цены на бокал пива:

In [20]:
catering[~catering['avg_bill'].isna() & catering['middle_avg_bill'].isna() 
         & catering['middle_coffee_cup'].isna() & catering['price'].isna()]['name'].count()
Out[20]:
16

Пропусков всего 16. Трудозатраты на их заполнение несоизмеримо больше, чем полученные результаты. Поэтому оставим их без изменений.

Оставшиеся пропуски, связанные с ценами, заполнить невозможно, поэтому оставим их без изменений.

Пропуски в количестве мест seats заполнить невозможно, поэтому оставим их без изменений.

Добавление новых столбцов¶

In [21]:
words = ['улица','ул','переулок','шоссе','проспект','площадь','проезд',
         'село','аллея','бульвар','набережная','тупик','линия', 'МКАД']

str_pat = r".*,\s*\b([^,]*?(?:{})\b[^,]*)[,$]+".format("|".join(words))

catering['street'] = catering['address'].str.extract(str_pat)
In [22]:
catering['is_24/7'] = catering['hours'].str.contains('ежедневно, круглосуточно') # столбец с обозначением о работе 24/7

В ходе предобработки были обнаружены неявные дубликаты, которые были удалены. В данных сразу в нескольких столбцах есть пропуски, удалось заполнить лишь несколько пропусков в ценовой категории заведений (чуть больше 700), остальные столбцы остались без изменений. Также были добавлены столбцы с названием улицы и обозначением о работе 24/7.

Исследовательский анализ данных¶

Распределение заведений по категориям¶

In [23]:
df_name = catering.groupby('category', as_index=False).agg({'name':'count'}).sort_values(by='name')
df_name
Out[23]:
category name
1 булочная 256
7 столовая 315
2 быстрое питание 603
5 пиццерия 633
0 бар,паб 764
4 кофейня 1413
6 ресторан 2039
3 кафе 2377
In [24]:
sns.set_style('darkgrid')
plt.subplots(figsize=(12,5))
sns.barplot(data = df_name, x='category',  y='name', color='b')
plt.xlabel('Тип заведения')
plt.ylabel('Количество')
plt.title('Распределение заведений по категориям')
r = np.arange(len(df_name['category']))
for x,y in zip(r, df_name['name']):

    label = "{:.0f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,1), 
                 ha='center')
plt.show()
No description has been provided for this image
In [25]:
plt.subplots(figsize=(10,7))
plt.pie(df_name['name'], labels = df_name['category'], autopct = '%.2f%%')
plt.title('Распределение заведений по категориям')
plt.show()
No description has been provided for this image

Лидером по количеству заведений в Москве является кафе (28,3%), далее следуют ресторан (24,27%) и кофейня (16,82%). Меньше всего булочных (3,05%).

Распределение посадочных мест по категориям¶

Количество посадочных мест будем анализировать с помощью среднего и медианы.

In [26]:
df_seats = catering.groupby('category', 
                            as_index=False).agg({'seats':['mean', 'median']}).droplevel(1, axis=1)
df_seats.columns = ['category', 'mean', 'median']
df_seats['mean'] = df_seats['mean'].apply(lambda x: round(x, 1))
df_seats = df_seats.sort_values(by='median')
df_seats
Out[26]:
category mean median
1 булочная 89.4 50.0
5 пиццерия 94.5 55.0
3 кафе 97.4 60.0
2 быстрое питание 98.9 65.0
7 столовая 99.8 75.5
4 кофейня 111.2 80.0
0 бар,паб 124.5 82.0
6 ресторан 121.9 86.0
In [27]:
n = len(df_seats) 
r = np.arange(n)
width = 0.25
fig, ax = plt.subplots(figsize=(15,8))
ax = plt.bar(r, df_seats['median'], width=width, label='median')
ax = plt.bar(r + width, df_seats['mean'], width=width, label='mean')
plt.xlabel('Категория')
plt.ylabel('Количество мест')
plt.title('Среднее и медиана посадочных мест по категориям')
plt.xticks(r + width/2, df_seats['category']) 
plt.legend()
for x,y in zip(r, df_seats['median']):

    label = "{:.1f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,5), 
                 ha='center')
for x,y in zip(r + width, df_seats['mean']):

    label = "{:.1f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,5), 
                 ha='center')
plt.show() 
No description has been provided for this image
In [28]:
plt.subplots(figsize=(12,7))
sns.boxplot(data=catering, x='category', y='seats')
plt.xlabel('Тип заведения')
plt.ylabel('Количество посадочных мест')
plt.title('Диаграмма размаха посадочных мест по категориям')
plt.show()
No description has been provided for this image

На графике видно, что в среднем меньше всего посадочных мест в булочных (медиана 50, средняя 89,4), а больше всего мест в ресторанах (медиана 86, средняя 121,9) и в барах (медиана 82 и средняя 124,5).
На ящиках с усами заметно, что 3-й квартиль доходит вплоть до 150, а значения больше 300 являются выбросами.

Распределение сетевых и несетевых заведений¶

In [29]:
df_chain = catering.groupby('chain', as_index=False).agg({'name': 'count'})
df_chain
Out[29]:
chain name
0 0 5197
1 1 3203
In [30]:
plt.subplots(figsize=(10,7))
plt.pie(df_chain['name'], labels = ['Несетевые', 'Сетевые'], autopct = '%.2f%%')
plt.title('Соотношение сетевых и несетевых заведений')
plt.show()
No description has been provided for this image

Сетевых заведений меньше (38,13%), а несетевых соответственно больше (61,87%).

In [31]:
df_category = catering.pivot_table(index='category', 
                                   values='name', columns='chain', aggfunc='count').reset_index()
df_category.columns = ['category', 'несетевые', 'сетевые']
df_category['chain_ratio'] = round(df_category['сетевые']/(df_category['сетевые']+df_category['несетевые'])*100,1)
df_category = df_category.sort_values(by='сетевые')
df_category
Out[31]:
category несетевые сетевые chain_ratio
7 столовая 227 88 27.9
1 булочная 99 157 61.3
0 бар,паб 596 168 22.0
2 быстрое питание 371 232 38.5
5 пиццерия 303 330 52.1
4 кофейня 693 720 51.0
6 ресторан 1310 729 35.8
3 кафе 1598 779 32.8
In [32]:
plt.subplots(figsize=(20, 10))
sns.barplot(data = df_category, x='category',  y='сетевые', ax=plt.subplot(1,2,1), color='g')
plt.xlabel('Тип заведения')
plt.ylabel('Количество сетевых заведений')
plt.title('Распределение сетевых заведений по категориям')
r = np.arange(len(df_category['category']))
for x,y in zip(r, df_category['сетевые']):

    label = "{:.0f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,5), 
                 ha='center')
    
sns.barplot(data = df_category, x='category',  y='chain_ratio', ax=plt.subplot(1,2,2), color='y')
plt.xlabel('Тип заведения')
plt.ylabel('Доля сетевых заведений')
plt.title('Доли сетевых заведений по категориям')
for x,y in zip(r, df_category['chain_ratio']):

    label = "{:.1f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,5), 
                 ha='center')
plt.tight_layout()
plt.show()
No description has been provided for this image
In [33]:
plt.subplots(figsize=(10,7))
plt.pie(df_category['сетевые'], labels = df_category['category'], autopct = '%.2f%%')
plt.title('Распределение сетевых заведений по категориям')
plt.show()
No description has been provided for this image

Больше всего сетевых заведений в абсолютном выражении встречается в категориях кафе (доля 24,32%), ресторан (22,76%), кофейня (22,48%), меньше всего - столовая (2,75%), булочная (4,9%), и бары (5,25%).
При этом по доле сетевых заведений лидером являются булочные (61,3% заведений сетевых), что в том числе обусловлено маленьким количеством данных заведений в целом. Далее следуют пиццерия (52,1%) и кофейня (51%).
Кофейня занимает 3-е место по количеству сетевых заведений как в абсолютном, так и в относительном выражении. Поэтому при открытии сетевого заведения стоит обратить внимание именно на данную категорию.

Топ-15 популярных сетей в Москве¶

In [34]:
df_top = catering[catering['chain']==1].groupby(
    'name', as_index=False).agg({'category':'first','chain':'count'}).sort_values(by='chain', ascending=False).head(15)
df_top
Out[34]:
name category chain
746 Шоколадница кофейня 120
344 Домино'с Пицца пиццерия 76
340 Додо Пицца пиццерия 74
148 One Price Coffee кофейня 71
759 Яндекс Лавка ресторан 69
59 Cofix кофейня 65
170 Prime ресторан 50
679 Хинкальная быстрое питание 44
378 КОФЕПОРТ кофейня 42
431 Кулинарная лавка братьев Караваевых кафе 39
643 Теремок ресторан 38
699 Чайхана кафе 37
40 CofeFest кофейня 32
273 Буханка булочная 32
491 Му-Му кафе 27
In [35]:
fig = px.bar(df_top.sort_values(by='chain'), 
             x='chain',
             y='name', 
             text='category'
            )

fig.update_layout(title='ТОП-15 сетей по количеству заведений',
                   xaxis_title='Количество заведений',
                   yaxis_title='Название сети')
fig.show()
In [37]:
df_cnt = df_top.groupby('category', as_index=False).agg({'name':'count'}).sort_values('name', ascending=False)
In [38]:
plt.subplots(figsize=(12,5))
sns.barplot(data = df_cnt, x='category',  y='name', color='r')
plt.xlabel('Тип заведения')
plt.ylabel('Количество')
plt.title('Распределение Топ-15 сетей по категориям')
plt.show()
No description has been provided for this image

Среди Топ-15 сетей на 1-ом месте с большим отрывом "Шоколадница" (120 заведений), далее следуют "Домино'с Пицца" (76) и "Додо Пицца" (74). В Топ-15 вошли самые разные категории, однако больше всего кофеен (5 сетей), кафе и ресторанов (по 3 сети).
Таким образом, в Топ-15 ярче всего представлены кофейни, а на 1-ом месте по количеству заведений сеть кофеен "Шоколадница". Это говорит о целесообразности открытия сети именно данной категории.

Распределение заведений по районам Москвы¶

In [39]:
df_district = catering.pivot_table(index='district', values='name', aggfunc='count', columns='category').reset_index()
In [40]:
def acronym(some_words): #функция для создания акронима
    return ''.join(re.findall(r'\b\w', some_words)).upper()
In [41]:
for row in range(len(df_district['district'])): #заменяем названия округов на акронимы
    df_district['district'] = df_district.replace(df_district.loc[row,'district'], acronym(df_district.loc[row,'district']))
In [42]:
df_district = df_district.set_index('district')
df_district['total'] = df_district.sum(axis=1)
In [43]:
df_district = df_district.sort_values(by='total')
df_district
Out[43]:
category бар,паб булочная быстрое питание кафе кофейня пиццерия ресторан столовая total
district
СЗАО 23 12 30 115 62 40 109 18 409
ЮЗАО 38 27 61 238 96 64 168 17 709
ЮВАО 38 13 67 282 89 55 144 25 713
ВАО 53 25 71 272 105 72 160 40 798
ЗАО 50 37 62 238 150 71 218 24 850
СВАО 62 28 82 269 159 68 181 40 889
ЮАО 68 25 85 264 131 73 202 44 892
САО 68 39 58 235 193 77 188 41 899
ЦАО 364 50 87 464 428 113 669 66 2241
In [44]:
plt.subplots(figsize=(12,7))
sns.heatmap(df_district.loc[:, df_district.columns != 'total'], annot=True, linewidths=1, fmt='d')
plt.xlabel('Категория')
plt.ylabel('Район')
plt.yticks(rotation = 360)
plt.title('Распределение категорий заведений по районам')
plt.show()
No description has been provided for this image
In [45]:
fig, ax = plt.subplots(figsize=(15,10))
ax1 = ax.bar(df_district.index, df_district['кафе'], label='кафе')
ax2 = ax.bar(df_district.index, df_district['ресторан'], bottom=df_district['кафе'], label='ресторан')
ax3 = ax.bar(df_district.index, df_district['кофейня'], bottom=df_district['кафе']+df_district['ресторан'], label='кофейня')
ax4 = ax.bar(df_district.index, df_district['бар,паб'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня'], label='бар,паб')
ax5 = ax.bar(df_district.index, df_district['пиццерия'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб'], label='пиццерия')
ax6 = ax.bar(df_district.index, df_district['быстрое питание'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб']+df_district['пиццерия'], label='быстрое питание')
ax7 = ax.bar(df_district.index, df_district['столовая'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб']+df_district['пиццерия']+df_district['быстрое питание'], label='столовая')
ax8 = ax.bar(df_district.index, df_district['булочная'], bottom=df_district['кафе']+df_district['ресторан']+df_district['кофейня']+df_district['бар,паб']+df_district['пиццерия']+df_district['быстрое питание']+df_district['столовая'], label='булочная')
ax.set_xlabel('Район')
ax.set_ylabel('Количество заведений')
ax.set_title('Распределение категорий заведений по районам')
plt.legend()
plt.show()
No description has been provided for this image

На тепловой карте и столбчатой диаграмме выделяется ЦАО, в нем заведений 2241, что более чем в 2 раза больше, чем в любом другом округе. Отдельно стоит выделить рестораны (669), кафе (464), кофейни (428), бары (364), цвет которых на тепловой карте является более светлым, чем все остальные ячейки.
Меньше всего заведений в СЗАО (409). В остальных округах заведений примерно поровну.
На столбчатой диаграмме хорошо видно, что рестораны, кафе и кофейни являются топ-3 категориями во всех округах. Это также может говорить о целесообразности открытия кофейни по сравнению с большинством других категорий, так как кофейни являются довольно популярными.

Распределение средних рейтингов по категориям заведений¶

In [46]:
df_rating = catering.groupby('category')['rating'].agg(['mean', 'median']).reset_index()
df_rating['mean'] = round(df_rating['mean'], 2)
df_rating=df_rating.sort_values(by='mean')
df_rating
Out[46]:
category mean median
2 быстрое питание 4.05 4.2
3 кафе 4.12 4.2
7 столовая 4.21 4.3
1 булочная 4.27 4.3
4 кофейня 4.28 4.3
6 ресторан 4.29 4.3
5 пиццерия 4.30 4.3
0 бар,паб 4.39 4.4

За основу для анализа возьмем среднюю, а не медиану, так как рейтинги весьма близки друг к другу и средняя лучше отражает отличие между категориями. Кроме того, средняя и медиана по всем категориям несильно отличаются друг от друга.

In [47]:
plt.subplots(figsize=(12,5))
sns.barplot(data = df_rating, x='category',  y='mean', color='k')
plt.ylim(4, 4.5)
plt.xlabel('Тип заведения')
plt.ylabel('Средний рейтинг')
plt.title('Средний рейтинг заведений по категориям')
r = np.arange(len(df_rating['category']))
for x,y in zip(r, df_rating['mean']):

    label = "{:.2f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,5), 
                 ha='center')
plt.show()
No description has been provided for this image
In [48]:
plt.subplots(figsize=(12,7))
sns.boxplot(data=catering, x='category', y='rating')
plt.xlabel('Тип заведения')
plt.ylabel('Рейтинг')
plt.title('Диаграмма размаха рейтинга по категориям')
plt.show()
No description has been provided for this image

На столбчатой диаграмме видно, что средние рейтинги по всем категориям находятся в диапазоне от 4 до 4,4, то есть отличаются несильно друг от друга. Однако четко прослеживаются лидер - бары (4,39) и аутсайдер - быстрое питание (4,05).
Кофейни находятся примерно посередине (4,28).
В кафе и барах нормальными считаются рейтинги вплоть до 5. Самый низкий 25-процентный квартиль у заведений быстрого питания.

Распределение средних рейтингов по районам Москвы¶

In [49]:
rating_df = catering.groupby('district')['rating'].agg(['median', 'mean']).reset_index()
rating_df['mean'] = round(rating_df['mean'], 2)
rating_df
Out[49]:
district median mean
0 Восточный административный округ 4.3 4.17
1 Западный административный округ 4.3 4.18
2 Северный административный округ 4.3 4.24
3 Северо-Восточный административный округ 4.2 4.15
4 Северо-Западный административный округ 4.3 4.21
5 Центральный административный округ 4.4 4.38
6 Юго-Восточный административный округ 4.2 4.10
7 Юго-Западный административный округ 4.3 4.17
8 Южный административный округ 4.3 4.18
In [50]:
with open('admin_level_geomap.geojson', 'r', encoding='utf-8') as f: # открываем файл geojson 
        state_geo = json.load(f)
In [51]:
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423

# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=rating_df,
    columns=['district', 'mean'], # за основу возьмем средний рейтинг, он лучше медианного отображает разницу между районами 
    key_on='feature.name',
    fill_color='YlOrRd',
    fill_opacity=0.8,
    legend_name='Средний рейтинг заведений по районам',
).add_to(m)

# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(m)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['rating']}",
    ).add_to(marker_cluster)
catering.apply(create_clusters, axis=1)
# выводим карту
m
Out[51]:
Make this Notebook Trusted to load map: File -> Trust Notebook

На фоновой картограмме видно, что самый высокий рейтинг (4,38) и наибольшее количество заведений (2241) находится в ЦАО, что соответствует выводам из прошлых графиков. Также на карте заметно постепенное падение числа заведений по мере отдаления от центра города. Самый низкий рейтинг у ЮВАО (4,1).

ЦАО выделяется по многим критериям: в нем больше всего количество заведений, самый высокий средний рейтинг, средний чек.
Высокое количество заведений связано с самым высоким спросом на услуги общепита по сравнению с другими районами. Самый высокий рейтинг говорит о высоком качестве предоставляемых услуг.
Высокий средний чек связан с более высоким уровнем цен на фоне более высокого спроса и качества услуг.
Другим фактором является то, что жители ЦАО в среднем богаче, чем жители остальных районов.
В ЦАО также расположены офисы многих престижных компаний, которые платят своим работникам хорошие зарплаты, которые потом в том числе тратятся в заведениях общепита.
ЦАО является центральным районом города, соответственно сюда приезжает большое число туристов, которые также тратят деньги в заведениях общепита. Из-за центрального расположения здесь также высокая стоимость аренды.

Распределение заведений по улицам Москвы¶

In [52]:
df_street = catering.pivot_table(index='street', values='name', aggfunc='count', columns='category').fillna(0).astype('int')
df_street['total'] = df_street.sum(axis=1)
df_top_street = df_street.sort_values(by='total', ascending=False).head(15).sort_values(by='total')
df_top_street
Out[52]:
category бар,паб булочная быстрое питание кафе кофейня пиццерия ресторан столовая total
street
Пятницкая улица 9 3 2 7 6 3 18 0 48
улица Миклухо-Маклая 3 0 4 21 4 2 15 0 49
Кутузовский проспект 2 1 2 14 13 3 16 3 54
улица Вавилова 2 2 11 15 10 3 12 0 55
Люблинская улица 5 0 5 26 11 1 10 2 60
МКАД 1 0 9 45 4 0 5 1 65
Ленинградское шоссе 5 2 5 13 13 3 25 3 69
Варшавское шоссе 6 0 7 17 14 4 20 7 75
Каширское шоссе 2 0 10 20 16 5 19 5 77
Дмитровское шоссе 6 2 10 23 11 8 24 4 88
Ленинградский проспект 15 4 2 12 25 9 25 3 95
Ленинский проспект 10 3 2 26 23 5 33 5 107
проспект Вернадского 7 1 12 25 16 12 33 2 108
Профсоюзная улица 6 4 15 35 18 15 26 3 122
проспект Мира 11 4 21 53 36 11 45 2 183
In [53]:
plt.subplots(figsize=(12,7))
sns.heatmap(df_top_street.loc[:, df_top_street.columns != 'total'], annot=True, linewidth=1, fmt='d')
plt.xlabel('Категория')
plt.ylabel('Улица')
plt.yticks(rotation = 360)
plt.title('Распределение категорий заведений по улицам')
plt.show()
No description has been provided for this image
In [54]:
fig, ax = plt.subplots(figsize=(15,10))
ax.barh(df_top_street.index, df_top_street['кафе'], label='кафе')
ax.barh(df_top_street.index, df_top_street['ресторан'], left=df_top_street['кафе'], label='ресторан')
ax.barh(df_top_street.index, df_top_street['кофейня'], left=df_top_street['кафе']+df_top_street['ресторан'], label='кофейня')
ax.barh(df_top_street.index, df_top_street['бар,паб'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня'], label='бар,паб')
ax.barh(df_top_street.index, df_top_street['пиццерия'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб'], label='пиццерия')
ax.barh(df_top_street.index, df_top_street['быстрое питание'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб']+df_top_street['пиццерия'], label='быстрое питание')
ax.barh(df_top_street.index, df_top_street['столовая'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб']+df_top_street['пиццерия']+df_top_street['быстрое питание'], label='столовая')
ax.barh(df_top_street.index, df_top_street['булочная'], left=df_top_street['кафе']+df_top_street['ресторан']+df_top_street['кофейня']+df_top_street['бар,паб']+df_top_street['пиццерия']+df_top_street['быстрое питание']+df_top_street['столовая'], label='булочная')
ax.set_xlabel('Количество заведений')
ax.set_ylabel('Улица')
ax.set_title('Распределение категорий заведений по улицам')
plt.legend()
plt.show()
No description has been provided for this image

Улицей с наибольшим количеством заведений является Проспект Мира (183). На тепловой карте самой светлой ячейкой является кафе на данной улице (53).
На столбчатой диаграмме видно, что в большинстве улиц самыми популярными являются кафе, рестораны и кофейни.

In [55]:
df_1 = df_street[df_street['total']==1]
df_total = pd.DataFrame(df_1.sum(axis=0)).sort_values(by=0).reset_index()
df_total.columns = ['category', 'count']
df_total = df_total[df_total.index!=8]
df_total
Out[55]:
category count
0 булочная 9
1 пиццерия 14
2 быстрое питание 17
3 столовая 36
4 бар,паб 40
5 кофейня 73
6 ресторан 88
7 кафе 137
In [56]:
plt.subplots(figsize=(12,5))
sns.barplot(data = df_total, x='category',  y='count', color='b')
plt.xlabel('Тип заведения')
plt.ylabel('Количество заведений')
plt.title('Распределение по категориям заведений на улицах с одним заведением')
r = np.arange(len(df_total['category']))
for x,y in zip(r, df_total['count']):

    label = "{:.0f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,3), 
                 ha='center')
plt.show()
No description has been provided for this image

На улицах с одним заведением преобладают кафе (137), рестораны (88), кофейни (73).

Стоимость среднего чека в зависимости от района¶

In [57]:
df_bill = catering.groupby('district').agg({'middle_avg_bill':'median'}).reset_index()
df_bill
Out[57]:
district middle_avg_bill
0 Восточный административный округ 575.0
1 Западный административный округ 1000.0
2 Северный административный округ 650.0
3 Северо-Восточный административный округ 500.0
4 Северо-Западный административный округ 700.0
5 Центральный административный округ 1000.0
6 Юго-Восточный административный округ 450.0
7 Юго-Западный административный округ 600.0
8 Южный административный округ 500.0
In [58]:
# создаём карту Москвы
k = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=df_bill,
    columns=['district', 'middle_avg_bill'], 
    key_on='feature.name',
    fill_color='YlGnBu',
    fill_opacity=0.8,
    legend_name='Средний чек заведений по районам',
).add_to(k)

# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(k)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['middle_avg_bill']}",
    ).add_to(marker_cluster)
catering.apply(create_clusters, axis=1)
# выводим карту
k
Out[58]:
Make this Notebook Trusted to load map: File -> Trust Notebook

На хороплете видно, что самый высокий средний чек в ЦАО и ЗАО (медиана равна 1000 рублей). Далее идут СЗАО (700 рублей) и САО (650 рублей). Меньше всего медиана среднего чека в ЮВАО (450). В целом по мере отдаления от центра стоимость среднего чека снижается.

Зависимость режима работы от категорий и районов¶

In [59]:
catering['hours'].nunique()
Out[59]:
1306
In [60]:
catering.groupby('hours').agg({'name':'count'}).sort_values(by='name', ascending=False).head(50)['name'].sum()
Out[60]:
5233

Все 1306 групп по режиму работы рассмотреть проблематично, да и не имеет смысла, так как многочисленные группы не удастся корректно отобразить и сравнить с малочисленными группами на одном графике.
Более того, многочисленные группы представляют собой более репрезентативную выборку, так как чем более малочисленной является группа, тем реже она встречается и тем слабее влияет на общее распределение данных.
Тенденции, которые можно заметить по многочисленным группам, чаще всего имеют место и в малочисленных группах. В небольшом количестве случаев, когда это не так, малочисленные группы все равно не смогут сильно повлиять на результат в силу маленького количества наблюдений в них.
На 20 наиболее многочисленных режимов работы приходится около половины (4204) совокупного количества наблюдений.
В целях анализа возьмем 50 наиболее распространенных режимов работы, этого вполне достаточно для выявления зависимостей между режимом работы и районом, категорией заведения.

In [61]:
df_work = catering.pivot_table(index='hours', values='name', aggfunc='count', columns='category').fillna(0)
df_work['total'] = df_work.sum(axis=1)
df_work = df_work.sort_values(by='total', ascending=False).head(50)
In [62]:
plt.subplots(figsize=(15,12))
sns.heatmap(df_work.loc[:, df_work.columns != 'total'], annot=True, fmt='.0f', linewidth=1)
plt.xlabel('Категория')
plt.ylabel('Режим работы')
plt.title('Распределение категорий заведений по режимам работы')
plt.show()
No description has been provided for this image

Наиболее часто встречаются работающие круглосуточно кафе (267), далее идут рестораны, работающие с 10 до 22 (192) или с 11 до 23 (175).
Бары и пабы в основном работают ежедневно в 12-часовом режиме с позднего утра (с 11 или 12) до позднего вечера (до 23 или 00).
Булочные в основном работают ежедневно с раннего утра (с 8 или с 9) до вечера (до 22 или 21). Также встречается круглосуточный режим работы.
Заведения быстрого питания работают ежедневно, многие круглосуточно или с 10 до 22.
Кафе работают в самые разные часы, однако чаще всего встречается круглосуточный режим работы или с 10 до 22.
Кофейни также работают в самое разное время, однако большая часть с утра до вечера (например с 10 до 22). Круглосуточные тоже встречаются.
Пиццерии работают в основном с 10 до 22 (или 23).
Рестораны работают в самое разное время, чаще с 10 до 22 или с 11 до 23. Встречается и круглосуточный режим работы.
Столовые работают только по будним дням с 9 до 18 (или до 17).

In [63]:
df_dst = catering.pivot_table(index='hours', values='name', aggfunc='count', columns='district').fillna(0)
counter=[]
for i in df_dst.columns.to_list(): # приводим названия районов к акронимам
    counter.append(acronym(i))
df_dst.columns = counter
df_dst['total'] = df_dst.sum(axis=1)
df_dst = df_dst.sort_values(by='total', ascending=False).head(50)
In [64]:
plt.subplots(figsize=(15,12))
sns.heatmap(df_dst.loc[:, df_dst.columns != 'total'], annot=True, fmt='.0f', linewidth=1)
plt.xlabel('Район')
plt.ylabel('Режим работы')
plt.title('Распределение районов по режимам работы заведений')
plt.show()
No description has been provided for this image

Тепловая карта распределения по районам немного более контрастная, чем карта по категориям. Это объясняется в том числе разницей в диапазонах разброса данных (в карте по районам диапазон значений примерно в 2 раза меньше, чем в карте по категориям).
На тепловой карте видно, что в ЦАО встречаются самые разные режимы работы, однако чаще всего круглосуточный (130 заведений), за ним идет с 12 до 0 (120). Такое разнообразие объясняется тем, что в ЦАО находится намного больше заведений, чем в любом другом районе (более чем в 2 раза). Поэтому для более тщательного сравнения других районов между собой (но такой цели не стоит) имеет смысл исключить ЦАО из анализа.
По аналогии "темнота" в СЗАО также объясняется сильно меньшим количеством заведений в данном районе по сравнению с другими.
В целом на данной карте очень отчетливо видно, что в Москве большинство заведений работает в круглосуточном режиме, либо с 10 до 22 (в том числе и в СЗАО).

Особенности заведений с плохими рейтингами¶

In [65]:
plt.subplots(figsize=(10,5))
sns.histplot(data=catering, x="rating", bins=10)
plt.xlabel('Рейтинг')
plt.ylabel('Количество')
plt.title('Распределение заведений по рейтингу')
plt.show()
No description has been provided for this image

На гистограмме видно, что у абсолютного большинства заведений рейтинг от 4 до 4,5, а по мере снижения рейтинга количество заведений падает. В качестве заведений с низким рейтингом целесообразно рассмотреть заведения с рейтингом меньше 3 баллов (3 балла делят выборку пополам).

In [66]:
g = sns.pairplot(catering, y_vars = ['rating'], x_vars=['middle_avg_bill', 'middle_coffee_cup', 'seats'])
g.fig.set_size_inches(15,5)
g.axes[0,0].set_ylabel('Рейтинг')
g.axes[0,0].set_xlabel('Средний чек')
g.axes[0,1].set_xlabel('Средняя цена чашки кофе')
g.axes[0,2].set_xlabel('Количество посадочных мест')
g.axes[0,1].set_title('Диаграммы рассеяния рейтинга заведений')
plt.show()
No description has been provided for this image
In [67]:
print(f'Корреляция между рейтингом и средним чеком: {round(catering["rating"].corr(catering["middle_avg_bill"]),4)}')
print(f'Корреляция между рейтингом и средней ценой чашки кофе: {round(catering["rating"].corr(catering["middle_coffee_cup"]),4)}')
print(f'Корреляция между рейтингом и количеством посдочных мест: {round(catering["rating"].corr(catering["seats"]),4)}')
Корреляция между рейтингом и средним чеком: 0.1832
Корреляция между рейтингом и средней ценой чашки кофе: 0.1004
Корреляция между рейтингом и количеством посдочных мест: 0.0211

На диаграммах рассеяния видно, что по заведениям с низким рейтингом наблюдений меньше, чем с высоким.
В заведениях с низким рейтиннгом средний чек не превышает 2500 рублей (есть одна аномалия с чеком около 5000 рублей).
Средняя цена чашки кофе не превышает 100 рублей.
Количество посадочных мест не превышает 400.
Корреляция между показателями положитальная слабая или практически отсутствует. Отметить только можно слабую прямую зависимость между рейтингом и средним чеком.

In [68]:
df_low = catering[catering['rating']<3].groupby('category').agg({'name':'count'}).reset_index().sort_values(by='name')
df_low
Out[68]:
category name
1 булочная 2
7 столовая 6
0 бар,паб 8
5 пиццерия 8
4 кофейня 16
2 быстрое питание 31
6 ресторан 32
3 кафе 107
In [69]:
plt.subplots(figsize=(12,5))
sns.barplot(data = df_low, x='category',  y='name', color='r')
plt.xlabel('Тип заведения')
plt.ylabel('Количество заведений')
plt.title('Распределение заведений с низким рейтингом по категориям')
r = np.arange(len(df_low['category']))
for x,y in zip(r, df_low['name']):

    label = "{:.0f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,3), 
                 ha='center')
plt.show()
No description has been provided for this image
In [70]:
plt.subplots(figsize=(10,7))
plt.pie(df_low['name'], labels = df_low['category'], autopct = '%.2f%%')
plt.title('Распределение заведений с низким рейтингом по категориям')
plt.show()
No description has been provided for this image

На столбчатой диаграмме видно, что абсолютное большинство заведений с низким рейтингом является кафе (доля 50,95%), также можно выделить заведения быстрого питания (14,76%) и рестораны (15,24%).

По итогам анализа можно подвести следующие итоги:

  • Лидером по количеству заведений в Москве является кафе (28,3%), далее следуют ресторан (24,27%) и кофейня (16,82%);
  • Посадочных мест меньше всего в булочных (медиана 50, средняя 89,4), а больше всего мест в ресторанах (медиана 86, средняя 121,9) и в барах (медиана 82 и средняя 124,5);
  • Сетевых заведений меньше (38,13%), а несетевых соответственно больше (61,87%);
  • Больше всего сетевых заведений в абсолютном выражении встречается в категориях кафе (доля 24,32%), ресторан (22,76%), кофейня (22,48%), меньше всего - столовая (2,75%), булочная (4,9%), и бары (5,25%);
  • По доле сетевых заведений лидером являются булочные (61,3% заведений сетевых), что в том числе обусловлено маленьким количеством данных заведений в целом. Далее следуют пиццерия (52,1%) и кофейня (51%);
  • Среди Топ-15 сетей на 1-ом месте с большим отрывом "Шоколадница" (120 заведений), далее следуют "Домино'с Пицца" (76) и "Додо Пицца" (74). В Топ-15 вошли самые разные категории, однако больше всего кофеен (5 сетей), кафе и ресторанов (по 3 сети);
  • Больше всего заведений в ЦАО (2241), меньше всего в СЗАО (409). В остальных округах заведений примерно поровну;
  • Средние рейтинги по всем категориям находятся в диапазоне от 4 до 4,4. Четко прослеживаются лидер - бары (4,39) и аутсайдер - быстрое питание (4,05).
  • На карте заметно постепенное падение числа заведений по мере отдаления от центра города;
  • По округам самый высокий рейтинг у ЦАО (4,38), а самый низкий - у ЮВАО (4,1);
  • Улицей с наибольшим количеством заведений является Проспект Мира (183, из них 53 - кафе);
  • На улицах с одним заведением преобладают кафе (137), рестораны (88), кофейни (73);
  • Cамый высокий средний чек в ЦАО и ЗАО (медиана равна 1000 рублей). Далее идут СЗАО (700 рублей) и САО (650 рублей). Меньше всего медиана среднего чека в ЮВАО (450). В целом по мере отдаления от центра стоимость среднего чека снижается;
  • Большинство заведений работает ежедневно с 10 до 22, также часто встречается режим работы 24/7. Также режим работы зависит от типа заведения. Например столовые работают в основном с 9 до 18 (или 17);
  • В заведениях с низким рейтиннгом средний чек не превышает 2500 рублей. Средняя цена чашки кофе не превышает 100 рублей. Количество посадочных мест не превышает 400;
  • Абсолютное большинство заведений с низким рейтингом является кафе (доля 50,95%), также можно выделить заведения быстрого питания (14,76%) и рестораны (15,24%).

Целесообразность открытия кофейни¶

Особенности расположения кофеен¶

In [71]:
df_cof = catering[catering["category"]=="кофейня"]
In [72]:
print(f'Количество кофеен: {df_cof["name"].count()}')
Количество кофеен: 1413
In [73]:
df_cofdst = df_cof.groupby('district', as_index=False).agg({'name':'count'}).sort_values(by='name')
for row in range(len(df_cofdst['district'])): #заменяем названия округов на акронимы
    df_cofdst['district'] = df_cofdst.replace(df_cofdst.loc[row,'district'], acronym(df_cofdst.loc[row,'district']))
df_cofdst = df_cofdst.merge(df_district['total'], on='district')
df_cofdst['ratio'] = round(df_cofdst['name']/df_cofdst['total']*100,2)
df_cofdst
Out[73]:
district name total ratio
0 СЗАО 62 409 15.16
1 ЮВАО 89 713 12.48
2 ЮЗАО 96 709 13.54
3 ВАО 105 798 13.16
4 ЮАО 131 892 14.69
5 ЗАО 150 850 17.65
6 СВАО 159 889 17.89
7 САО 193 899 21.47
8 ЦАО 428 2241 19.10
In [74]:
plt.subplots(figsize=(20,10))
sns.barplot(data = df_cofdst, x='district',  y='name', ax=plt.subplot(1,2,1), color = 'y')
plt.xlabel('Район')
plt.ylabel('Количество кофеен')
plt.title('Распределение кофеен по районам')
r = np.arange(len(df_cofdst['district']))
for x,y in zip(r, df_cofdst['name']):

    label = "{:.0f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,3), 
                 ha='center')
sns.barplot(data = df_cofdst, x='district',  y='ratio', ax=plt.subplot(1,2,2), color = 'g')
plt.xlabel('Район')
plt.ylabel('Доля кофеен, %')
plt.title('Доли кофеен по районам')
for x,y in zip(r, df_cofdst['ratio']):

    label = "{:.2f}".format(y)

    plt.annotate(label,
                 (x, y), 
                 textcoords="offset points", 
                 xytext=(0,3), 
                 ha='center')
plt.tight_layout
plt.show()
No description has been provided for this image
In [75]:
plt.subplots(figsize=(10,7))
plt.pie(df_cofdst['name'], labels = df_cofdst['district'], autopct = '%.2f%%')
plt.title('Распределение кофеен по районам')
plt.show()
No description has been provided for this image

Большая часть кофеен находится в ЦАО (доля 30,29%), далее идут САО (13,66%) и СВАО (11,25%).
По доле кофеен от всех заведений ЦАО (19,10%) занимает 2-е место после САО (21,47%). На 3-ем и 4-ом местах СВАО (17,89%) и ЗАО (17,65%).

Режим работы кофеен¶

In [76]:
print(f'Количество круглосуточных кофеен: {df_cof[df_cof["is_24/7"]==True]["name"].count()}')
print(f'Доля круглосуточных кофеен: {round(df_cof["is_24/7"].mean()*100,2)}%')
Количество круглосуточных кофеен: 59
Доля круглосуточных кофеен: 4.22%
In [77]:
df_24 = df_cof.groupby('is_24/7').agg({'name':'count'})
In [78]:
plt.subplots(figsize=(10,7))
plt.pie(df_24['name'], labels = ['Некруглосуточный', 'Круглосуточный'], autopct = '%.2f%%')
plt.title('Режим работы кофеен')
plt.show()
No description has been provided for this image

4,22% из всех кофеен (по которым есть данные) работают круглосуточно.

In [79]:
df_chain = df_cof.groupby('chain').agg({'name':'count'})
In [80]:
plt.subplots(figsize=(10,7))
plt.pie(df_chain['name'], labels = ['Несетевые', 'Сетевые'], autopct = '%.2f%%')
plt.title('Сетевые и несетевые кофейни')
plt.show()
No description has been provided for this image

Сетевых и несетевых кофеен примерно поровну, 49,04% и 50,96% соответственно.

Распределение рейтингов кофеен по районам¶

In [81]:
cof_rat = df_cof.groupby('district').agg({'rating': ['mean', 'median']}).reset_index().droplevel(1, axis=1)
cof_rat.columns=['district', 'mean', 'median']
cof_rat['mean'] = round(cof_rat['mean'], 2)
cof_rat
Out[81]:
district mean median
0 Восточный административный округ 4.28 4.3
1 Западный административный округ 4.20 4.2
2 Северный административный округ 4.29 4.3
3 Северо-Восточный административный округ 4.22 4.3
4 Северо-Западный административный округ 4.33 4.3
5 Центральный административный округ 4.34 4.3
6 Юго-Восточный административный округ 4.23 4.3
7 Юго-Западный административный округ 4.28 4.3
8 Южный административный округ 4.23 4.3

Медианный рейтинг почти во всех районах одинаковый, поэтому будем использовать среднюю, чтобы отразить отличие между районами.

In [82]:
# создаём карту Москвы
a = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=cof_rat,
    columns=['district', 'mean'], 
    key_on='feature.name',
    fill_color='Greens',
    fill_opacity=0.8,
    legend_name='Средний рейтинг заведений по районам',
).add_to(a)

# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(a)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['rating']}",
    ).add_to(marker_cluster)
df_cof.apply(create_clusters, axis=1)
# выводим карту
a
Out[82]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Максимально высокий средний рейтинг в ЦАО (4,34) и в СЗАО (4,33). В целом рейтинги по районам мало отличаются друг от друга, они находятся в диапазоне от 4,2 до 4,34.

Распределение средней стоимости чашки капучино по районам¶

In [83]:
cof_cup = df_cof.groupby('district').agg({'middle_coffee_cup': ['mean', 'median']}).reset_index().droplevel(1, axis=1)
cof_cup.columns=['district', 'mean', 'median']
cof_cup['mean'] = round(cof_cup['mean'], 2)
cof_cup
Out[83]:
district mean median
0 Восточный административный округ 174.02 135.0
1 Западный административный округ 189.94 189.0
2 Северный административный округ 165.79 159.0
3 Северо-Восточный административный округ 165.33 162.5
4 Северо-Западный административный округ 165.52 165.0
5 Центральный административный округ 187.52 190.0
6 Юго-Восточный административный округ 151.09 147.5
7 Юго-Западный административный округ 184.18 198.0
8 Южный административный округ 158.49 150.0
In [84]:
# создаём карту Москвы
b = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')

# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
    geo_data=state_geo,
    data=cof_cup,
    columns=['district', 'median'], 
    key_on='feature.name',
    fill_color='RdPu',
    fill_opacity=0.8,
    legend_name='Медианная стоимость чашки капучино заведений по районам',
).add_to(b)

# добавляем пустой кластер на карту
marker_cluster = MarkerCluster().add_to(b)
# создаём маркер и добавляем его в кластер
def create_clusters(row):
    Marker(
        [row['lat'], row['lng']],
        popup=f"{row['name']} {row['middle_coffee_cup']}",
    ).add_to(marker_cluster)
df_cof.apply(create_clusters, axis=1)
# выводим карту
b
Out[84]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Максимальная медианная стоимость чашки капучино в ЦАО (190), ЗАО (189), ЮЗАО (198). В целом она находится в диапазоне от 135 до 198.

Рекомендации по открытию кофейни¶

  • Наиболее популярным районом является ЦАО (428 кофейни). Большое количество заведений означает серьезную конкуренцию, но открытие кофейни в данном районе может окупиться за счет большого спроса. Наименьшее количество кофеен в СЗАО (62), возможно здесь удастся заполнить пустующую нишу. Однако можно открыть кофейню и в любом другом районе - главное выбрать удачное место с высокой проходимостью;
  • Абсолютное большинство кофеен (более 95%) работает не в круглосуточном режиме, что подтверждает нерентабельность круглосуточного режима работы кофейни. Что касается сетей, то результаты анализа говорят о равном шансе на успех при открытии сетевого (51%) или несетевого (49%) заведения;
  • Рейтинги заведений по районам мало отличаются друг от друга, однако в ЦАО (4,34) и СЗАО (4,33) они наибольшие;
  • В ЦАО медианная стоимость чашки капучино равна 190, что является одним из самых больших показателей. При открытии кофейни в любом из районов следует ориентироваться именно на медианную стоимость чашки в этом районе.

Таким образом, можно рассмотреть бизнес-проект с открытием кофейни в ЦАО, работающую ежедневно с 10 до 22. Стоимость чашки капучино при этом должна быть равна примерно 190. Количество посадочных мест зависит от планируемой проходимости и спроса.
Отдельно стоит отметить недостаток данных в датасете. Для корректного анализа инвестиционного проекта необходимы некоторые другие показатели, которых в данных нет. Например, средняя проходимость, объем затрат на аренду, затрат на заработную плату и т.д. Без них невозможно корректно рассчитать денежные потоки и срок окупаемости инвестиций.

Выводы¶

В ходе предобработки данных были выявлены следующие проблемы:

  • Неявные дубликаты (6), которые были удалены;
  • Пропуски, по ценовой категории удалось восстановить около 700 пропусков, остальные пропуски остались без изменения;
  • Тип данных количества посадочных мест был изменен на целочисленный.

По итогам анализа можно подвести следующие итоги:

  • Лидером по количеству заведений в Москве является кафе (28,3%), далее следуют ресторан (24,27%) и кофейня (16,82%);
  • Посадочных мест меньше всего в булочных (медиана 50, средняя 89,4), а больше всего мест в ресторанах (медиана 86, средняя 121,9) и в барах (медиана 82 и средняя 124,5);
  • Сетевых заведений меньше (38,13%), а несетевых соответственно больше (61,87%);
  • Больше всего сетевых заведений в абсолютном выражении встречается в категориях кафе (доля 24,32%), ресторан (22,76%), кофейня (22,48%), меньше всего - столовая (2,75%), булочная (4,9%), и бары (5,25%);
  • По доле сетевых заведений лидером являются булочные (61,3% заведений сетевых), что в том числе обусловлено маленьким количеством данных заведений в целом. Далее следуют пиццерия (52,1%) и кофейня (51%);
  • Среди Топ-15 сетей на 1-ом месте с большим отрывом "Шоколадница" (120 заведений), далее следуют "Домино'с Пицца" (76) и "Додо Пицца" (74). В Топ-15 вошли самые разные категории, однако больше всего кофеен (5 сетей), кафе и ресторанов (по 3 сети);
  • Больше всего заведений в ЦАО (2241), меньше всего в СЗАО (409). В остальных округах заведений примерно поровну;
  • Средние рейтинги по всем категориям находятся в диапазоне от 4 до 4,4. Четко прослеживаются лидер - бары (4,39) и аутсайдер - быстрое питание (4,05).
  • На карте заметно постепенное падение числа заведений по мере отдаления от центра города;
  • По округам самый высокий рейтинг у ЦАО (4,38), а самый низкий - у ЮВАО (4,1);
  • Улицей с наибольшим количеством заведений является Проспект Мира (183, из них 53 - кафе);
  • На улицах с одним заведением преобладают кафе (137), рестораны (88), кофейни (73);
  • Cамый высокий средний чек в ЦАО и ЗАО (медиана равна 1000 рублей). Далее идут СЗАО (700 рублей) и САО (650 рублей). Меньше всего медиана среднего чека в ЮВАО (450). В целом по мере отдаления от центра стоимость среднего чека снижается;
  • Большинство заведений работает ежедневно с 10 до 22, также часто встречается режим работы 24/7. Также режим работы зависит от типа заведения. Например столовые работают в основном с 9 до 18 (или 17);
  • В заведениях с низким рейтиннгом средний чек не превышает 2500 рублей. Средняя цена чашки кофе не превышает 100 рублей. Количество посадочных мест не превышает 400;
  • Абсолютное большинство заведений с низким рейтингом является кафе (доля 50,95%), также можно выделить заведения быстрого питания (14,76%) и рестораны (15,24%).

Рекомендации по открытию кофейни:

  • Район с наибольшим спросом - ЦАО (428 кофейни) или с наименьшей конкуренцией - СЗАО (62);
  • Режим работы - ежедневно с 10 до 22;
  • Стоимость чашки капучино - до 200 рублей;
  • Количество посадочных мест зависит от планируемой проходимости.